/*
 * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.xuexiang.xdlink.component.channel.impl.usb;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.support.annotation.NonNull;

import com.ftdi.j2xx.D2xxManager;
import com.ftdi.j2xx.FT_Device;
import com.xuexiang.xdlink.component.channel.IChannel;
import com.xuexiang.xdlink.component.channel.OnChannelListener;
import com.xuexiang.xdlink.component.channel.entity.ConnectStatusEnum;
import com.xuexiang.xdlink.component.channel.impl.AbstractChannel;
import com.xuexiang.xdlink.component.channel.impl.AbstractDataReceiver;
import com.xuexiang.xdlink.component.channel.impl.serialport.SerialPortParam;
import com.xuexiang.xdlink.exception.ChannelException;
import com.xuexiang.xdlink.logs.DCFLog;
import com.xuexiang.xdlink.util.DCFExecutors;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * FTDI USB驱动
 *
 * @author xuexiang
 * @since 2018/9/16 下午4:55
 */
public class FtdiUSBChannel extends AbstractChannel {

    private Context mContext;
    /**
     * 厂家标识
     */
    private int mVendorId;
    /**
     * 产品标识
     */
    private int mProductId;
    /**
     * 串口参数
     */
    private SerialPortParam mSerialPortParam;

    //=========================//
    /**
     * FTDI驱动
     */
    private D2xxManager mD2xxManager;
    /**
     * FTDI设备
     */
    private FT_Device mFTDevice;
    /**
     * 扫描到FTDI设备的数量
     */
    private int mDevCount = -1;
    /**
     * 所有扫描到的FTDI设备集合
     */
    private ArrayList mFtdiDevices;

    /**
     * 当前已打开设备的索引
     */
    private int mCurrentIndex = -1;
    /**
     * 打开设备的索引
     */
    private int mOpenIndex = 0;
    /**
     * 是否注册USB事件拔插广播
     */
    private boolean mIsRegister;

    /**
     * 构造函数
     *
     * @param builder
     */
    public FtdiUSBChannel(Builder builder) {
        this(builder.context, builder.vendorId, builder.productId, builder.serialPortParam);
    }

    /**
     * 构造函数
     *
     * @param context
     * @param vendorId        厂家标识
     * @param productId       产品标识
     * @param serialPortParam 串口参数
     */
    public FtdiUSBChannel(@NonNull Context context, int vendorId, int productId, SerialPortParam serialPortParam) {
        mContext = context;
        mVendorId = vendorId;
        mProductId = productId;
        mSerialPortParam = serialPortParam;

        initUSB();
    }

    @Override
    public boolean isOpen() {
        return super.isOpen() && mFTDevice != null && mFTDevice.isOpen();
    }

    @Override
    public void openChannel() {
        DCFExecutors.get().addTask(new Runnable() {
            @Override
            public void run() {
                initDeviceList();

                if (mDevCount > 0) {
                    connectUSB();
                } else {
                    checkUsbDevice(); // 三次检测(连接)设备
                    if (mDevCount > 0) {
                        connectUSB();
                    } else {
                        DCFLog.e("USB[ftdi] No equipment detected!");
                        updateConnectStatus(ConnectStatusEnum.CONNECT_FAILED);
                    }
                }
            }
        });
    }

    @Override
    public void closeChannel() {
        unRegisterUSBBroadcast();
        close();
    }

    @Override
    public void write(byte[] message) throws ChannelException {
        writeBytes(message);
    }

    //===========================初始化===============================//

    /**
     * 初始化USB
     */
    private void initUSB() {
        initD2xxManager();
        setVID_PID(mVendorId, mProductId);
    }

    /**
     * 初始化USB
     *
     * @param vendorId  厂家标识
     * @param productId 产品标识
     */
    protected void initUSB(int vendorId, int productId) {
        initD2xxManager();
        setVID_PID(vendorId, productId);
    }

    /**
     * 初始化FTDI驱动
     */
    private void initD2xxManager() {
        try {
            mD2xxManager = D2xxManager.getInstance(mContext);
        } catch (D2xxManager.D2xxException ex) {
            DCFLog.e("USB[ftdi] Loading FTDI Driver failed!", ex);
            ex.printStackTrace();
        }
    }

    /**
     * 设置VID、PID
     *
     * @param vendorId  厂家标识
     * @param productId 产品标识
     */
    private void setVID_PID(int vendorId, int productId) {
        try {
            boolean res = mD2xxManager.setVIDPID(vendorId, productId);
            DCFLog.d("USB[ftdi] Set VID: " + vendorId + "、PID:" + productId + " " + (res ? "success" : "failed") + "!");
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化USB设备列表
     */
    private void initDeviceList() {
        int tempDevCount = mD2xxManager.createDeviceInfoList(mContext);
        if (tempDevCount > 0) {
            if (mDevCount != tempDevCount) {
                mDevCount = tempDevCount;
            }
        } else {
            mDevCount = -1;
            mCurrentIndex = -1;
        }
        mFtdiDevices = getFtdiDevices(mD2xxManager);
        DCFLog.d("USB[ftdi] Initialize usb device list, device count:" + mDevCount);
    }

    //===========================打开===============================//

    /**
     * 三次检测(连接)设备
     */
    private void checkUsbDevice() {
        int count = 0;
        while (count < 3) {
            initDeviceList();
            if (mDevCount > 0) {
                break;
            } else {
                count++;
            }
        }
        DCFLog.d("USB[ftdi] Results of the three test equipment：" + mDevCount);
    }

    /**
     * 连接USB
     */
    private void connectUSB() {
        boolean openResult = openUSBDevice();
        updateOpenedStatus(openResult);
        if (openResult) {
            registerUSBBroadcast();
        } else {
            updateConnectStatus(ConnectStatusEnum.CONNECT_FAILED);
        }
    }

    /**
     * 打开USB设备
     *
     * @return 是否成功打开设备
     */
    private boolean openUSBDevice() {
        if (mCurrentIndex != mOpenIndex) {
            if (mDevCount > 0) {
                List<Integer> openList = findOpenableIndex(mD2xxManager);
                if (openList != null && openList.size() > 0) {
                    mOpenIndex = openList.get(0); //打开合适的第一个设备
                }
            }
            DCFLog.d("USB[ftdi] Open USB index：" + mOpenIndex);
            if (mFTDevice == null) {
                mFTDevice = mD2xxManager.openByIndex(mContext, mOpenIndex);
            } else {
                synchronized (mFTDevice) {
                    mFTDevice = mD2xxManager.openByIndex(mContext, mOpenIndex);
                }
            }
        } else {
            DCFLog.i("USB[ftdi] The device has been opened!");
            updateConnectStatus(ConnectStatusEnum.ALREADY_CONNECTED);
            return true;
        }


        if (mFTDevice == null) {
            DCFLog.e("USB[ftdi] Failed to turn on device, mFTDevice == null");
            return false;
        }

        if (mFTDevice.isOpen()) {
            DCFLog.d("USB[ftdi] Device opened successfully!");
            setSerialPortDeviceConfigures();
            startAcceptData(new USBDataReceiver(getChannelListener()));
            mCurrentIndex = mOpenIndex;
            return true;
        } else {
            DCFLog.e("USB[ftdi] Failed to open device port!");
            return false;
        }
    }

    /**
     * 寻找可以打开USB的所有索引
     *
     * @param d2xxManager
     * @return
     */
    private List<Integer> findOpenableIndex(D2xxManager d2xxManager) {
        List<Integer> openIndexList = new ArrayList<>();
        D2xxManager.FtDeviceInfoListNode deviceListNode;
        for (int i = 0; i < mDevCount; i++) {
            deviceListNode = d2xxManager.getDeviceInfoListDetail(i);
            if (deviceListNode != null) {
                if (deviceListNode.description != null) {// 过滤掉非OTG连接的设备
                    FT_Device ftDevice = (FT_Device) mFtdiDevices.get(i);
                    UsbDevice usbDevice = getUsbDevice(ftDevice);
                    if (usbDevice != null && isChannelUSBDevice(usbDevice)) {
                        openIndexList.add(i);
                    }
                }
            }
        }
        return openIndexList;
    }

    /**
     * 注册USB设备拔插事件广播
     */
    private void registerUSBBroadcast() {
        if (!mIsRegister) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
            filter.setPriority(100);
            mContext.registerReceiver(mUsbReceiver, filter);
            mIsRegister = true;
        }
    }

    /**
     * 注销USB设备拔插事件广播
     */
    private void unRegisterUSBBroadcast() {
        if (mIsRegister) {
            mContext.unregisterReceiver(mUsbReceiver);
            mIsRegister = false;
        }
    }

    /**
     * 监听USB设备拔插事件的广播
     */
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {// USB断开广播
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (isChannelUSBDevice(device) && isOpenedUSBChannelDevice(device)) {
                    closeChannel();
                    updateConnectStatus(ConnectStatusEnum.DISCONNECT_SUCCESS);
                }
            }
        }
    };

    /**
     * 判断usb设备是不是指定厂家的设备
     *
     * @param device
     * @return
     */
    private boolean isChannelUSBDevice(UsbDevice device) {
        int vendorId = device.getVendorId();// 厂商ID
        int productId = device.getProductId();// 产品ID
        return vendorId == mVendorId && productId == mProductId;
    }

    /**
     * 判断是否是当前通道打开的USB设备
     *
     * @param device
     * @return
     */
    private boolean isOpenedUSBChannelDevice(UsbDevice device) {
        if (mFTDevice != null && mFTDevice.isOpen()) {
            UsbDevice openDevice = getUsbDevice(mFTDevice);
            if (openDevice != null) {
                return openDevice.equals(device);
            }
        }
        return false;
    }

    //===========================关闭===============================//

    /**
     * 关闭数据通道连接
     */
    @Override
    protected void close() {
        DCFLog.i(toString() + " is closing...");
        stopAcceptData();
        updateOpenedStatus(false);
        mDevCount = -1;
        mCurrentIndex = -1;

        if (mFtdiDevices != null && mFtdiDevices.size() > 0) {
            mFtdiDevices.clear();
            mFtdiDevices = null;
        }
        if (mFTDevice != null && mFTDevice.isOpen()) {
            mFTDevice.close();
            mFTDevice = null;
        }
    }

    //===========================写数据===============================//

    /**
     * 发送数据
     *
     * @param message
     * @return
     */
    private boolean writeBytes(byte[] message) {
        if (mDevCount <= 0 || mFTDevice == null || !mFTDevice.isOpen()) {
            DCFLog.e("USB[ftdi] Sending message failed，USB device is not opened");
            return false;
        }
        mFTDevice.setLatencyTimer((byte) 16);
        int result = mFTDevice.write(message, message.length);
        if (result == message.length) {
            DCFLog.i("USB[ftdi] data sent successfully，data length：" + result);
            return true;
        } else {
            DCFLog.e("USB[ftdi] data sent failed，data length：" + result);
            return false;
        }
    }

    //===========================数据接收===============================//

    /**
     * USB数据接收器
     *
     * @author xuexiang
     * @since 2018/9/16 下午11:12
     */
    protected class USBDataReceiver extends AbstractDataReceiver {

        public USBDataReceiver(OnChannelListener listener) {
            super(null, 5000, listener);
        }

        @Override
        protected void onStart() throws IOException {
            mFTDevice.purge((byte) (D2xxManager.FT_PURGE_TX | D2xxManager.FT_PURGE_RX));
            /*
             * 保证发消息和清空消息、接收消息在同一个线程中。
             */
            updateConnectStatus(ConnectStatusEnum.CONNECT_SUCCESS);
        }

        @Override
        public void acceptData() throws Exception {
            if (!isOpen()) return;

            mInAvailable = mFTDevice.getQueueStatus();
            if (mInAvailable > 0) {
                if (mInAvailable > mReadData.length) {
                    mInAvailable = mReadData.length;
                }
                mInAvailable = mFTDevice.read(mReadData, mInAvailable);
                onDataReceived(mReadData, mInAvailable);
            }
        }

        @Override
        public IChannel getChannel() {
            return FtdiUSBChannel.this;
        }
    }

    //===========================参数设置===============================//

    /**
     * 设置设备串口参数
     */
    protected void setSerialPortDeviceConfigures() {
        if (mSerialPortParam == null) return;

        setSerialPortConfig(mSerialPortParam.getBaud(), mSerialPortParam.getDataBits(), mSerialPortParam.getStopBits(), mSerialPortParam.getParity(), mSerialPortParam.getFlowControl());
    }

    /**
     * 设置串口参数
     *
     * @param baud        波特率
     * @param dataBits    数据位
     * @param stopBits    停止位
     * @param parity      校验位
     * @param flowControl 流控制
     */
    protected void setSerialPortConfig(int baud, byte dataBits, byte stopBits, byte parity, byte flowControl) {
        if (mFTDevice == null || !mFTDevice.isOpen()) {
            DCFLog.e("USB[ftdi] setSerialPortConfig: device not open");
            return;
        }
        // configure our port
        mFTDevice.setBitMode((byte) 0, D2xxManager.FT_BITMODE_RESET);
        mFTDevice.setBaudRate(baud);
        switch (dataBits) {
            case 7:
                dataBits = D2xxManager.FT_DATA_BITS_7;
                break;
            case 8:
                dataBits = D2xxManager.FT_DATA_BITS_8;
                break;
            default:
                dataBits = D2xxManager.FT_DATA_BITS_8;
                break;
        }
        switch (stopBits) {
            case 1:
                stopBits = D2xxManager.FT_STOP_BITS_1;
                break;
            case 2:
                stopBits = D2xxManager.FT_STOP_BITS_2;
                break;
            default:
                stopBits = D2xxManager.FT_STOP_BITS_1;
                break;
        }
        switch (parity) {
            case 0:
                parity = D2xxManager.FT_PARITY_NONE;
                break;
            case 1:
                parity = D2xxManager.FT_PARITY_ODD;
                break;
            case 2:
                parity = D2xxManager.FT_PARITY_EVEN;
                break;
            case 3:
                parity = D2xxManager.FT_PARITY_MARK;
                break;
            case 4:
                parity = D2xxManager.FT_PARITY_SPACE;
                break;
            default:
                parity = D2xxManager.FT_PARITY_NONE;
                break;
        }
        mFTDevice.setDataCharacteristics(dataBits, stopBits, parity);
        short flowCtrlSetting;
        switch (flowControl) {
            case 0:
                flowCtrlSetting = D2xxManager.FT_FLOW_NONE;
                break;
            case 1:
                flowCtrlSetting = D2xxManager.FT_FLOW_RTS_CTS;
                break;
            case 2:
                flowCtrlSetting = D2xxManager.FT_FLOW_DTR_DSR;
                break;
            case 3:
                flowCtrlSetting = D2xxManager.FT_FLOW_XON_XOFF;
                break;
            default:
                flowCtrlSetting = D2xxManager.FT_FLOW_NONE;
                break;
        }
        mFTDevice.setFlowControl(flowCtrlSetting, (byte) 0x0b, (byte) 0x0d);
    }

    //================反射======================//

    /**
     * 根据ftDevice反射获取UsbDevice
     *
     * @param ftdiD2xx
     * @return
     */
    private ArrayList getFtdiDevices(D2xxManager ftdiD2xx) {
        try {
            Class cls = D2xxManager.class;
            Field field = cls.getDeclaredField("mFtdiDevices");
            field.setAccessible(true);
            Object object = field.get(ftdiD2xx);
            return (ArrayList) object;
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据ftDevice反射获取UsbDevice，以获取FT_Device正在打开的USB设备
     *
     * @param ftDevice
     * @return
     */
    private UsbDevice getUsbDevice(FT_Device ftDevice) {
        try {
            Class cls = FT_Device.class;
            Field field = cls.getDeclaredField("mUsbDevice");
            field.setAccessible(true);
            Object object = field.get(ftDevice);
            return (UsbDevice) object;
        } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    //========================构建者==========================//

    /**
     * 构建者
     */
    public static class Builder {
        Context context;
        /**
         * 厂家标识
         */
        int vendorId = -1;
        /**
         * 产品标识
         */
        int productId = -1;

        SerialPortParam serialPortParam;

        public Builder(@NonNull Context context) {
            this.context = context.getApplicationContext();
        }

        public Builder setVendorId(int vendorId) {
            this.vendorId = vendorId;
            return this;
        }

        public Builder setProductId(int productId) {
            this.productId = productId;
            return this;
        }

        public Builder setSerialPortParam(SerialPortParam serialPortParam) {
            this.serialPortParam = serialPortParam;
            return this;
        }

        public FtdiUSBChannel build() {
            testArguments();
            return new FtdiUSBChannel(this);
        }

        private void testArguments() {
            if (this.vendorId == -1) {
                throw new IllegalArgumentException("USB[ftdi] vendorId must be set！");
            }
            if (this.productId == -1) {
                throw new IllegalArgumentException("USB[ftdi] productId must be set！");
            }
        }
    }

}
